package com.opensource.svgaplayer;

import com.opensource.svgaplayer.iml.ParseCompletion;
import com.opensource.svgaplayer.proto.MovieEntity;
import com.opensource.svgaplayer.utils.Intrinsics;
import com.opensource.svgaplayer.utils.log.LogUtil;

import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.net.HttpResponseCache;
import ohos.utils.zson.ZSONObject;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.Inflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * ex.
 *
 * @author wl
 * @since 2021-06-28
 */
public class SVGAParser {
    private final AtomicInteger fileLock = new AtomicInteger(0) ;
    private boolean isUnzipping = false;

    /**
     * SVGAParser
     *
     * @param context Context
     */
    public SVGAParser(Context context) {
        this.mContext = context != null ? context.getApplicationContext() : null;
        SVGACache.getInstance().onCreate(context);
        this.fileDownloader = new SVGAParser.FileDownloader();
    }

    private Context mContext;
    private volatile int mFrameWidth;
    private volatile int mFrameHeight;
    private SVGAParser.FileDownloader fileDownloader;
    private static final String TAG = "SVGAParser";
    private static final String TAG1 = "1110000111";
    private static final AtomicInteger threadNum = new AtomicInteger(0);
    private static SVGAParser mShareParser = new SVGAParser(null);
    private static ExecutorService threadPoolExecutor = Executors.newCachedThreadPool(new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "SVGAParser-Thread-" + threadNum.getAndIncrement());
        }
    });

    private static void setThreadPoolExecutor(ThreadPoolExecutor executor) {
        threadPoolExecutor = executor;
    }

    /**
     * getThreadPoolExecutor
     *
     * @return ExecutorService
     */
    public static ExecutorService getThreadPoolExecutor() {
        return threadPoolExecutor;
    }

    /**
     * shareParser
     *
     * @return SVGAParser
     */
    public static SVGAParser shareParser() {
        return mShareParser;
    }

    /**
     * SVGAParser.FileDownloader
     *
     * @return getFileDownloader
     */
    public final SVGAParser.FileDownloader getFileDownloader() {
        return this.fileDownloader;
    }

    /**
     * setFileDownloader
     *
     * @param var1 SVGAParser.FileDownloader
     */
    public final void setFileDownloader(SVGAParser.FileDownloader var1) {
        Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
        this.fileDownloader = var1;
    }

    /**
     * init
     *
     * @param context Context
     */
    public final void init(Context context) {
        Intrinsics.checkParameterIsNotNull(context, "context");
        this.mContext = context.getApplicationContext();
        SVGACache.getInstance().onCreate(this.mContext);
    }

    /**
     * setFrameSize
     *
     * @param frameWidth  int
     * @param frameHeight int frameHeight
     */
    public final void setFrameSize(int frameWidth, int frameHeight) {
        this.mFrameWidth = frameWidth;
        this.mFrameHeight = frameHeight;
    }

    /**
     * decodeFromURL
     *
     * @param url      URL
     * @param callback ParseCompletion
     */
    public void decodeFromURL(URL url, ParseCompletion callback) {
        if (mContext == null) {
            return;
        }
        String cacheKey = SVGACache.getInstance().buildCacheKey(url);
        if (SVGACache.getInstance().isCached(cacheKey)) {
            threadPoolExecutor.execute(new Runnable() {
                public final void run() {
                    if (SVGACache.getInstance().isDefaultCache()) {
                        decodeFromCacheKey(cacheKey, callback);
                    } else {
                        _decodeFromCacheKey(cacheKey, callback);
                    }
                }
            });
            return;
        } else {
            fileDownloader.resume(url, cacheKey, callback);
        }
    }

    private void decodeFromCacheKey(String cacheKey, ParseCompletion callback) {
        if (this.mContext == null) {
            return;
        }
        try {
            File cacheDir = SVGACache.getInstance().buildCacheDir(cacheKey);
            File file = new File(cacheDir, "movie.binary");
            FileInputStream fileInputStream = null;
            if (file != null && file.isFile()) {
                try {
                    LogUtil.loge(TAG1, "binary change to entity");
                    fileInputStream = new FileInputStream(file);
                    MovieEntity decode = MovieEntity.ADAPTER.decode(fileInputStream);
                    Intrinsics.checkExpressionValueIsNotNull(decode, "MovieEntity.ADAPTER.decode(it)");
                    this.invokeCompleteCallback(new SVGAVideoEntity(decode, cacheDir, this.mFrameWidth, this.mFrameHeight), callback);
                } catch (Exception e) {
                    boolean delete = file.delete();
                    if (fileInputStream!=null){
                        fileInputStream.close();
                    }
                    e.printStackTrace();
                }
            }
            File spec = new File(cacheDir, "movie.spec");
            if (spec != null && spec.isFile()) {
                try {
                    fileInputStream = new FileInputStream(spec);
                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                    byte[] buffer = new byte[2048];
                    while (true) {
                        int size = fileInputStream.read(buffer, 0, buffer.length);
                        if (size == -1) {
                            String byteStr = byteArrayOutputStream.toString();
                            ZSONObject jsonObject = ZSONObject.stringToZSON(byteStr);
                            LogUtil.loge("SVGAParser", "spec change to entity success");
                            this.invokeCompleteCallback(new SVGAVideoEntity(jsonObject, cacheDir, this.mFrameWidth, this.mFrameHeight), callback);
                            break;
                        }
                        byteArrayOutputStream.write(buffer, 0, size);
                    }
                } catch (Exception e) {
                    e.printStackTrace();

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            this.invokeErrorCallback(e, callback);
        }
    }

    private void invokeCompleteCallback(SVGAVideoEntity videoItem, ParseCompletion callback) {
        new EventHandler(EventRunner.getMainEventRunner()).postTask(new Runnable() {
            @Override
            public void run() {
                if (callback != null) {
                    callback.onComplete(videoItem);
                }
            }
        });
    }

    private void invokeErrorCallback(Exception exception, ParseCompletion callback) {
        exception.printStackTrace();
        LogUtil.loge(TAG, "================ parser error ================");
        LogUtil.loge(TAG, "error");
        new EventHandler(EventRunner.getMainEventRunner()).postTask(new Runnable() {
            @Override
            public void run() {
                if (callback != null) {
                    callback.onError();
                }
            }
        });
    }

    /**
     * _decodeFromCacheKey
     *
     * @param cacheKey String
     * @param callback ParseCompletion
     */
    public final void _decodeFromCacheKey(String cacheKey, ParseCompletion callback) {
        Intrinsics.checkParameterIsNotNull(cacheKey, "cacheKey");
        File svga = SVGACache.getInstance().buildSvgaFile(cacheKey);
        try {
            FileInputStream inputStream = new FileInputStream(svga);
            try {
                byte[] bytes = this.readAsBytes(inputStream);
                if (bytes != null) {
                    //  LogUtil.loge("SVGAParser", "cache.inflate start");
                    byte[] inflateBytes = this.inflate(bytes);
                    if (inflateBytes != null) {
                        //   LogUtil.loge("SVGAParser", "cache.inflate success");
                        Object var10002 = MovieEntity.ADAPTER.decode(inflateBytes);
                        Intrinsics.checkExpressionValueIsNotNull(var10002, "MovieEntity.ADAPTER.decode(inflateBytes)");
                        SVGAVideoEntity videoItem = new SVGAVideoEntity((MovieEntity) var10002, new File(cacheKey), this.mFrameWidth, this.mFrameHeight);
                        videoItem.prepare(videoItem, callback);
                    } else {
                        doError("cache.inflate(bytes) cause exception", callback);
                    }
                } else {
                    doError("cache.readAsBytes(inputStream) cause exception", callback);
                }
            } catch (Exception exception) {
                this.invokeErrorCallback(exception, callback);
            } finally {
                inputStream.close();
            }
        } catch (Exception exception) {
            LogUtil.loge("SVGAParser", "cache.binary change to entity fail");
            if (svga != null && svga.exists()) {
                boolean delete = svga.delete();
            }
            this.invokeErrorCallback(exception, callback);
        }
    }

    /**
     * doError
     *
     * @param error    String
     * @param callback ParseCompletion
     */
    public final void doError(String error, ParseCompletion callback) {
        Intrinsics.checkParameterIsNotNull(error, "error");
        LogUtil.loge("SVGAParser", error);
        this.invokeErrorCallback(new Exception(error), callback);
    }

    /**
     * parse
     *
     * @param assetsName String
     * @param callback   ParseCompletion
     * @deprecated from 2.4.0
     */
    public void parse(String assetsName, ParseCompletion callback) {
        this.decodeFromAssets(assetsName, callback);
    }

    /**
     * parse
     *
     * @param url      URL
     * @param callback ParseCompletion
     * @deprecated from 2.4.0
     */
    public void parse(URL url, ParseCompletion callback) {
        this.decodeFromURL(url, callback);
    }

    /**
     * parse
     *
     * @param inputStream      InputStream
     * @param cacheKey         cacheKey
     * @param callback         ParseCompletion
     * @param closeInputStream boolean
     * @deprecated from 2.4.0
     */
    public void parse(InputStream inputStream, String cacheKey, ParseCompletion callback, boolean closeInputStream) {
        this.decodeFromInputStream(inputStream, cacheKey, callback, closeInputStream);
    }

    /**
     * decodeFromAssets
     *
     * @param name     String
     * @param callback ParseCompletion
     */
    public void decodeFromAssets(String name, ParseCompletion callback) {
        if (mContext == null) {
            LogUtil.loge(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。");
            return;
        }
        try {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    InputStream inputStream = null;
                    try {
                        inputStream = mContext.getResourceManager().getRawFileEntry("resources/rawfile/" + name).openRawFile();
                        if (inputStream != null) {
                            decodeFromInputStream(inputStream, SVGACache.getInstance().buildCacheKey("file:///assets/" + name), callback, true);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });

        } catch (Exception e) {
            this.invokeErrorCallback(e, callback);
        }
    }

    /**
     * 读取解析来自URL的svga文件.并缓存成本地文件
     *
     * @param inputStream InputStream
     * @param cacheKey    String
     * @param callback    ParseCompletion
     */
    public void _decodeFromInputStream(InputStream inputStream, String cacheKey, ParseCompletion callback) {
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    byte[] bytes = readAsBytes(inputStream);
                    if (bytes != null) {
                        threadPoolExecutor.execute(new Runnable() {
                            @Override
                            public void run() {
                                File cacheFile = SVGACache.getInstance().buildSvgaFile(cacheKey);
                                if (!cacheFile.exists()) {
                                    try {

                                        boolean newFile = cacheFile.createNewFile();
                                        if (newFile){
                                            new FileOutputStream(cacheFile).write(bytes);
                                        }
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                        boolean delete = cacheFile.delete();
                                    }
                                }
                            }
                        });
                        LogUtil.loge(TAG, "Input.inflate start");
                        byte[] inflateBytes = inflate(bytes);
                        if (inflateBytes != null) {
                            LogUtil.loge(TAG, "Input.inflate success");
                            SVGAVideoEntity videoItem = new SVGAVideoEntity(
                                    MovieEntity.ADAPTER.decode(inflateBytes),
                                    new File(cacheKey),
                                    mFrameWidth,
                                    mFrameHeight
                            );
                            invokeCompleteCallback(videoItem, callback);
                        } else {
                            doError("Input.readAsBytes(inputStream) cause exception", callback);
                        }
                    }
                } catch (Exception e) {

                }
            }
        });
    }

    /**
     * decodeFromInputStream
     *
     * @param inputStream      InputStream
     * @param cacheKey         String
     * @param callback         ParseCompletion
     * @param closeInputStream boolean
     */
    public void decodeFromInputStream(InputStream inputStream, String cacheKey,
                                      ParseCompletion callback, boolean closeInputStream) {
        if (mContext == null) {
            LogUtil.loge(TAG, "在配置 SVGAParser context 前, 无法解析 SVGA 文件。");
            return;
        }
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    byte[] bytes = readAsBytes(inputStream);
                    if (bytes != null) {
                        if (bytes.length > 4 && bytes[0] == 80 && bytes[1] == 75 && bytes[2] == 3 && bytes[3] == 4) {
                            LogUtil.loge(TAG1, "decode from zip file");
                            if (!SVGACache.getInstance().buildCacheDir(cacheKey).exists() || isUnzipping) {
                                synchronized (fileLock) {
                                    if (!SVGACache.getInstance().buildCacheDir(cacheKey).exists()) {
                                        isUnzipping = true;
                                        LogUtil.loge(TAG1, "no cached, prepare to unzip");
                                        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
                                        unzip(byteArrayInputStream, cacheKey);
                                        isUnzipping = false;
                                        LogUtil.loge(TAG1, "unzip success");
                                    }
                                }
                            }
                            decodeFromCacheKey(cacheKey, callback);
                        } else {
                            LogUtil.loge(TAG1, "decode from input stream, inflate start");
                            byte[] bytes1 = inflate(bytes);
                            if (bytes1 != null) {

                                SVGAVideoEntity svgaVideoEntity = new SVGAVideoEntity(MovieEntity.ADAPTER.decode(bytes1), new File(cacheKey), mFrameWidth, mFrameHeight);
                                svgaVideoEntity.prepare(svgaVideoEntity, callback);
                            } else {
                                invokeErrorCallback(
                                        new Exception("inflate(bytes) cause exception"),
                                        callback
                                );
                            }
                        }
                    } else {
                        invokeErrorCallback(new Exception("readAsBytes(inputStream) cause exception"), callback);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (closeInputStream) {
                        try {
                            inputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
    }

    private byte[] readAsBytes(InputStream inputStream) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            byte[] byteArray = new byte[2048];
            while (true) {
                int count = inputStream.read(byteArray, 0, 2048);
                if (count <= 0) {
                    byte[] bytes = byteArrayOutputStream.toByteArray();
                    return bytes;
                }
                byteArrayOutputStream.write(byteArray, 0, count);
            }
        } catch (Exception e) {
            return null;
        } finally {
            byteArrayOutputStream.close();
        }
    }

    private final byte[] inflate(byte[] byteArray) {
        Inflater inflater = new Inflater();
        inflater.setInput(byteArray, 0, byteArray.length);
        byte[] inflatedBytes = new byte[2048];
        ByteArrayOutputStream inflatedOutputStream = new ByteArrayOutputStream();
        try {
            while (true) {
                int count = inflater.inflate(inflatedBytes, 0, 2048);
                if (count <= 0) {
                    break;
                } else {
                    inflatedOutputStream.write(inflatedBytes, 0, count);
                }
            }
            inflater.end();
            return inflatedOutputStream.toByteArray();
        } catch (Exception e) {
            return null;
        } finally {
            try {
                inflatedOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private final void unzip(InputStream inputStream, String cacheKey) {
        File cacheDir = SVGACache.getInstance().buildCacheDir(cacheKey);
        boolean mkdirs = cacheDir.mkdirs();
        if (!mkdirs){
            LogUtil.loge("cacheDir make dir fail");
        }
        BufferedInputStream it = new BufferedInputStream(inputStream);
        ZipInputStream zipInputStream1 = new ZipInputStream(it);
        try {
            ZipInputStream zipInputStream = zipInputStream1;
            while (true) {
                ZipEntry zipEntry = zipInputStream.getNextEntry();
                if (zipEntry == null) {
                    break;
                }
                String name = zipEntry.getName();
                Intrinsics.checkExpressionValueIsNotNull(name, "zipItem.name");
                if (name.contains("../")) {
                    // 解压路径存在路径穿越问题，直接过滤
                    continue;
                }
                if (name.contains("/")) {
                    continue;
                }
                FileOutputStream fileOutputStream = null;
                File file = new File(cacheDir, zipEntry.getName());
                try {
                    fileOutputStream = new FileOutputStream(file);
                    byte[] buff = new byte[2048];
                    while (true) {
                        int readBytes = zipInputStream.read(buff);
                        if (readBytes <= 0) {
                            break;
                        }
                        fileOutputStream.write(buff, 0, readBytes);
                    }
                } catch (IOException e) {
                    LogUtil.loge(e.getMessage());
                } finally {
                    if (fileOutputStream!=null){
                        fileOutputStream.close();
                    }
                }
                zipInputStream.closeEntry();
            }
        } catch (IOException e) {
            LogUtil.loge(e.getMessage());
        } finally {
            try {
                zipInputStream1.close();
            } catch (IOException ioException) {
                LogUtil.loge(ioException.getMessage());
            }
        }
        boolean delete = cacheDir.delete();
        if (!delete){
            LogUtil.loge("cacheDir delete dir fail");
        }
    }

    public class FileDownloader {
        private boolean noCache;

        /**
         * getNoCache
         *
         * @return boolean
         */
        public boolean getNoCache() {
            return this.noCache;
        }

        /**
         * setNoCache
         *
         * @param noCache boolean
         */
        public void setNoCache(boolean noCache) {
            this.noCache = noCache;
        }

        /**
         * resume
         *
         * @param url      URL
         * @param cacheKey String
         * @param callback ParseCompletion
         */
        public void resume(URL url, String cacheKey, ParseCompletion callback) {
            boolean cancelled = false;
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (HttpResponseCache.getInstalled() == null && !noCache) {
                            LogUtil.loge(TAG, "在配置 HttpResponseCache 前 SVGAParser 无法缓存. 查看 https://gitee.com/chinasoft5_ohos/SVGAPlayer-Ohos.git#cache ");
                        }
                        URLConnection urlConnection = url.openConnection();
                        if (urlConnection != null && urlConnection instanceof HttpURLConnection) {
                            urlConnection.setConnectTimeout(20 * 1000);
                            ((HttpURLConnection) urlConnection).setRequestMethod("GET");
                            urlConnection.connect();
                            InputStream inputStream = urlConnection.getInputStream();
                            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                            byte[] buffer = new byte[4096];
                            while (true) {
                                if (cancelled) {
                                    break;
                                }
                                int count = inputStream.read(buffer, 0, 4096);
                                if (count == -1) {
                                    break;
                                }
                                outputStream.write(buffer, 0, count);
                            }
                            if (cancelled) {
                                return;
                            }
                            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(outputStream.toByteArray());
                            if (SVGACache.getInstance().isDefaultCache()) {
                                decodeFromInputStream(byteArrayInputStream, cacheKey, callback, true);
                            } else {
                                _decodeFromInputStream(byteArrayInputStream, cacheKey, callback);
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        invokeErrorCallback(e, callback);
                    }
                }
            });
        }
    }
}
